一路上感謝各位讀者們的支持和回饋。
本 30 天系列文目前已經將篇幅重新整理、編纂成冊。
《JavaScript 概念三明治》在天瓏書局上架囉!
喜歡這個系列,想閱讀更詳細原理說明的讀者可以參考:
https://www.tenlong.com.tw/products/9789864347575
今天要講的是是兩個在操作物件時常用到的 JS API ,有時候我們會需要做一些比較進階的操作,例如對物件屬性做一些比較細節的微調;還有複製物件,但是複製物件的話,因為物件傳參考的特性的關係,在結構複雜的物件上,往往需要特別處理,例如物件內的屬性是另外一個物件。所以我們也會帶到「深拷貝」和「淺拷貝」的概念。
Object.defineProperty
其實是 Object
函式建構子上的靜態方法(還記得 Obejct 其實是一個函式?),用來對某個物件直接定義一個新的屬性,用法如下:
const object1 = {};
Object.defineProperty(object1, 'property1', {
value: 42,
writable: false
});
這個方法接受三個參數,第一個是要新增屬性的目標物件,第二個是屬性名稱,第三個是這個屬性的描述器設定。 屬性的描述器?那是什麼?
JS 內物件屬性的描述器有兩種類型,每一種各有不同設定值:
資料描述器 ( Data descriptor ):
資料描述器是一個帶有值的屬性,其實也就是你要定義屬性的 value
啦。這個屬性有可能是可修改、或是不可被修改的。
存取描述器 ( Accessor descriptor ):
存取描述器定義的內容包含的 getter
與 setter
兩個函式。要怎麼存、取這個屬性,就是由存取描述器來負責的。
兩種描述器都有屬於自己的屬性設定值,先分別介紹:
true
就代表這個屬性可以透過 如 ob.name= 'new value'
被更新。getter
函式,是一個定義物件如何被取用的函式,當物件屬性被取用的時候會被呼叫。setter
函式,是一個定義物件如何被指派的函式。剩下的幾個設定值是兩種描述器都能夠使用且可選、非必須的。分別是( 括號內的是預設值 ):
enumerable
、 writable
、 或是自己本身 configurable
。Object.keys
或是 for...in
來對物件作遍歷的時候能不能夠存取到。而要定義的物件屬性的描述器必須一定要是上述兩者中的其中一種,兩者無法同時屬於兩者。
var o = {}; // 創造新物件
Object.defineProperty(o, 'a', {}); // empty descriptor setting
Object.getOwnPropertyDescriptor(o,'a')
//預設描述器值:
// configurable : false
// value : undefined
// writable : false
// enumable : false
剛剛說描述器無法同時是資料描述器跟存取描述器,也就是說在 ****defineProperty
的第三個參數描述器設定內,如果有 get
這個設定值出現,就不能再有 value
,否則就會報錯:
var o = {}; // 創造新物件
Object.defineProperty(o, 'a', {
value: 37,
writable: true,
enumerable: true,
configurable: true,
get(){
return 123
}
});
//Invalid property descriptor. Cannot both specify accessors and a value or writable attribute
getter
與 setter
函式自訂 getter
與 setter
一樣是在 Object.defineProperty
裡面的第三個參數自訂屬性的行為:
var o = {};
Object.defineProperty(o, 'a', {
get() {
return 'It will return this value if I access o.a' ;
},
set() {
this.myname = 'this is my name string';
}
});
Object.assign
用來複製所有物件內可被尋訪 (Enumable) 的屬性,而且複製的來源不限於某個物件,可以多個物件一起進行屬性的複製,這個方法的第一個參數跟 defineProperty
ㄧ樣都是目標物件,後面可以有複數個參數,就是要被複製屬性的來源。而使用 Object.assign
來進行複製的時候,後面的相同物件屬性會蓋掉前面相同的物件屬性:
let b = Object.assign({foo: 0}, {foo: 1}, {foo: 2});
ChromeSamples.log(b)
// {foo: 2}
所以,如果我想要複製某一物件的內容到一個全新的物件上的話,只要這麼寫:
let oldObject = {
a:'a',
b:{
c:'cinsideb'
}
}
let newObject = Object.assign({},oldObject)
console.log(newObject) //{a: "a", b: {…}}
另外,如果只是單純要把某個物件內容複製到另外一個物件,可以用 ES6 後的新的、比較簡潔好閱讀的寫法 Spread ,也可以達到一樣的效果:
let newObject = { ...oldObject }
在使用 Object.assign
時有一個要注意的地方,就是他雖然可以複製屬性,但要是物件屬性的內容也是另外一個物件時,從這個屬性複製到新物件上的,也只會是這個內層物件的參考,而不是這個物件的拷貝,這個現象就稱為淺拷貝(可以理解為,只複製最外層屬性,往下被複製的都只有參考)。
let oldObject = {
a:'a',
b:{
c:'c'
}
}
let newObject = Object.assign({},oldObject)
newObject.b.c = 'modified c'
console.log(oldObject)
/* {
a:'a',
b:{
c:'modified c'
}
} */
由上就可以看出,當我修改新的物件的內層屬性物件時,被複製的物件的內層屬性物件 (b.c
),也會跟著一起被改動。
相對於淺拷貝,深拷貝就是完全的複製整個物件內容了。那麼如果要達到這個效果,我們可能要自己動手處理,檢查要複製物件的某屬性是不是物件,如果是的話,就要再以Object.assgn
複製一次,並且這個檢查要搭配遞迴的概念來檢查,才能確保完全的複製。
function cloneDeep(obj){
if( typeof obj !== 'object' ){
return obj
}
let resultData = {}
return recursion(obj, resultData)
}
function recursion(obj, data={}){
//對物件屬性做巡訪
for(key in obj){
if( typeof obj[key] === 'object'){
// 如果是物件就繼續往下遞迴
data[key] = recursion(obj[key])
}else{
// 如果不是物件的話就直接指派
data[key] = obj[key]
}
}
return data
}
let player = {name:'Anakin',friend:{robot:'R2D2'}}
let player2 = cloneDeep(player)
obj.name = 'Darth Vader!!!'
player2.friend.robot = 'no!!!'
console.log(player) // {name:'Anakin猿',friend:{robot:'R2D2'}}
Javascript properties are enumerable, writable and configurable
JS-淺拷貝(Shallow Copy) VS 深拷貝(Deep Copy)